home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Language/OS - Multiplatform Resource Library
/
LANGUAGE OS.iso
/
smaltalk
/
st80_vw.lha
/
st80_vw
/
SmallDraw.VW
< prev
next >
Wrap
Text File
|
1993-07-24
|
94KB
|
2,423 lines
" NAME SmallDraw.VW
AUTHOR Dan Benson (modifications by David Arctur)
CONTRIBUTOR Dan Benson <dbenson@scr.siemens.com>
FUNCTION Simple Structured Graphics Editor
ST-VERSIONS ST-80 VisualWorks
PREREQUISITES
CONFLICTS
DISTRIBUTION world
VERSION 2.0
DATE June 25, 1993
SUMMARY
The SmallDraw application is an example of graphics rendering and MVC
application construction in Smalltalk-80. Originally written to run
under Release 4.0, this version has been modified by David Arctur to
run under VisualWorks (Thank you David!) See SmallDraw class comments
for a description of the modifications.
This file contains the complete implementation of SmallDraw as described
in the third of a series of three tutorial articles entitled 'SmallDraw -
Release 4 Graphics and MVC, Part 3', published in the August 1992
issue of The Smalltalk Report, edited by John Pugh & Paul White, SIGS
Publications Group, Inc. For a more detailed description, please
refer to the cited article.
This source code is available to all with no restrictions. I only ask
that proper credit be passed on so that I might hear from those who
have found SmallDraw useful.
Dan Benson
"!
ApplicationModel subclass: #SmallDraw
instanceVariableNames: 'objects insideColor borderColor lineWidth pages drawPane scalePane hAlignTo vAlignTo hAlign vAlign '
classVariableNames: 'Clipboard '
poolDictionaries: ''
category: 'SmallDraw'
"
The following instance variables are inherited by this class:
ApplicationModel -- builder
Model -- dependents
Object --
"!
SmallDraw comment:
'SmallDraw is a very simple structured-graphics editor written by:
Dan Benson
Siemens Corporate Research, Inc.
755 College Road East
Princeton, NJ 08540
dbenson@scr.siemens.com
This source code is available to all with no restrictions. I only ask that
proper credit be passed on so that I might hear from those who have found
SmallDraw useful.
This file contains the complete implementation of SmallDraw as described in the
third of a series of tutorial articles entitled "SmallDraw - Release 4 Graphics
and MVC, Part 3", published in the August 1992 issue of The Smalltalk Report,
edited by John Pugh & Paul White, SIGS Publications Group, Inc.
The SmallDraw application is an example of graphics rendering in Smalltalk-80
Release 4. The first article in the series contains an introduction to graphics
concepts and application construction with the MVC architecture through the
definition of a ''minimal'' SmallDraw. The second article adds the ability to
select and modify objects in the view. The third article extends the features
of SmallDraw to include grouping of objects, scrolling of the view,
cut/copy/pasting, alignment of objects through a DialogView, layering of
objects, and use of command keys. This filein corresponds to the implementation
of the application described in the third and final article.
For a more detailed description, please refer to the above cited article.
MODIFICATIONS:
This file was modified in June ''93 to work with VisualWorks 1.0 & Smalltalk-80 Release 4.1 by:
David Arctur
Department of Urban & Regional Planning
431 ARCH, University of Florida
Gainesville, FL 32611-2004
arctur@nervm.nerdc.ufl.edu
The VisualWorks version may be executed from the Finder (Launcher button),
or by evaluating "SmallDraw open".
VisualWorks follows a somewhat different approach in the MVC coupling than
before. The SmallDraw class (subclass of ApplicationModel) now holds onto
instance variables for each of the subviews in the window (drawPane and
scalePane), and coordinates actions between them. Different window layouts
on the model would be accomplished by making up additional "windowSpecs"
with the UI builder; these could add to, or ignore, subviews used by other
windowSpecs.
In updating SmallDraw, I found that a number of things which had worked in
Release 4, no longer worked under VisualWorks. These included:
- deepCopy (I reimplemented this locally for the drawing clipboard)
- scrolling control (now handled transparently via the
SmallDrawView>>preferredBounds method)
- alignment dialog (I rebuilt & simplified this using the UI builder)
In addition, numerous minor syntax changes were required. While I was at it,
I changed the names and groupings of the protocol categories somewhat
(my apologies to Dan).
Please let me know if you have trouble with this, or make further changes to it.
-------------------------------------------------
NOTICE: This file defines the following category and classes:
(''SmallDraw'' #SDEllipse #SDGraphicObject #SDGraphicGroup #SDLineSegment
#SDPolygon #SDPolyline #SDQuadrangle #SmallDraw #SmallDrawController
#SmallDrawView)
and adds a Point instance method:
quadrantContaining: aPoint
-------------------------------------------------
SmallDraw Description:
This MVC application consists of a simple model called SmallDraw that opens
with a window containing a scalable graphics view, SmallDrawView. The user can
select from a menu one of several shapes to draw in the view using the mouse.
Drawing is controlled by the SmallDrawController which uses a rubber banding
technique for dynamic display. Figures drawn in the view are added to a list of
objects maintained by the model. All graphic objects have an inside color,
border color, and line width, and know how to display themselves in a view at a
given scale.
Individual objects may be selected by either clicking on their interiors (if
solid) or edges. A range of objects may be selected by drawing a rubber-banding
rectangle around them. Once selected, the visual attributes of objects may be
changed or they may be translated or scaled.
To begin a SmallDraw application, select and do the following:
SmallDraw open
Also, see the other methods in the ''window creation'' category of SmallDraw.
SmallDraw has the following instance variables:
objects <OrderedCollection> the list of objects in the drawing (Associations)
insideColor <ColorValue> the default inside color for new objects
borderColor <ColorValue> the default border color for new objects
lineWidth <Integer> the default line width for new objects
pages <Point> indicates the number of pages in horizontal and
vertical directions
drawPane <ValueHolder on: SmallDrawView> holds onto an instance of SmallDrawView
scalePane <ValueHolder on: ComposedTextView> used to display the current scale value
The next four instance variables are set during the alignment dialog:
vAlign <ValueHolder on: Boolean> vertical alignment {true|false}
vAlignTo <ValueHolder on: Symbol> vertical alignment {#top|#center|#bottom}
hAlign <ValueHolder on: Boolean> horizontal alignment {true|false}
hAlignTo <ValueHolder on: Symbol> horizontal alignment {#left|#center|#right}
SmallDraw has the following class variables:
Clipboard <Object> intermediate storage of objects for
cut/copy/paste/duplicate
The objects instance variable actually stores a collection of Associations in
which the value is the graphic object and the value is a Boolean that indicates
whether or not the object is currently selected.
As objects are added to the drawing they are layered on top of each other.
Their postions within this stack may be changed through a menu selection.
Most menu operations apply only to the current selection of objects. For
instance, the selected objects can be aligned, grouped, ungrouped, copied, and
cut, or their visual aspects can be modified.
A SmallDraw document consists of one or more pages. Objects are not allowed to
be translated or scaled beyond the topmost or left most pages of the document.
If this happens, a warning bell is rung and the operation is not permitted to
continue. If objects are moved beyond the rightmost or bottommost pages, the
total size of the document is automatically grown to fit the object''s new
postions. The document may be resized to its minimum configuration (to include
all existing objects) through a menu selection.
Objects are cut/copied/pasted via the common class variable Clipboard. In this
way, all SmallDraw applications have access to the same Clipboard.
'!
!SmallDraw methodsFor: 'window creation'!
initialize
super initialize.
objects := OrderedCollection new.
insideColor := nil.
borderColor := ColorValue black.
lineWidth := 1.
pages := self minimumPages.
drawPane := SmallDrawView new
model: self ;
controller: SmallDrawController new .
self scalePane: 100.!
openIt
"SmallDraw new openIt"
ScheduledWindow new
label: 'SmallDraw';
component: (WidgetWrapper on:
( (SmallDrawView model: self) controller: SmallDrawController new));
openWithExtent: 200@200!
openWithTwoViews
"SmallDraw new openWithTwoViews"
| window composite|
window := ScheduledWindow new label: 'SmallDraw'.
composite := CompositePart new.
window component: composite.
composite
"The left hand view."
add: (WidgetWrapper on:
((SmallDrawView model: self) controller: SmallDrawController new))
in: (LayoutFrame new
leftFraction: 0;
rightFraction: 0.5;
topFraction: 0;
bottomFraction: 1);
"The right hand view."
add: (WidgetWrapper on:
((SmallDrawView model: self) controller: SmallDrawController new))
in: (LayoutFrame new
leftFraction: 0.5;
rightFraction: 1;
topFraction: 0;
bottomFraction: 1).
window openWithExtent: 200@200! !
!SmallDraw methodsFor: 'aspects'!
drawPane
^drawPane!
scalePane
"This method was generated by UIDefiner. The initialization provided
below may have been preempted by an initialize method."
^scalePane isNil ifTrue: [scalePane := String new asValue] ifFalse: [scalePane]!
scalePane: aNumber
"display the current view's scale value"
aNumber rounded = aNumber
ifTrue: [self scalePane value: aNumber printString, '%']
ifFalse: [self scalePane value: aNumber asFloat printString, '%']! !
!SmallDraw methodsFor: 'menu'!
processCommandKey: aKey
"Respond to aKey which may corrsepond to one of the receiver's menu
commands. If not, ignore it."
aKey = Character backspace ifTrue: [self delete].
aKey = $x ifTrue: [self cut].
aKey = $c ifTrue: [self copy].
aKey = $v ifTrue: [self paste].
aKey = $f ifTrue: [self moveForward].
aKey = $j ifTrue: [self moveBackward].
aKey = $d ifTrue: [self duplicate].
aKey = $a ifTrue: [self selectAll].
aKey = $k ifTrue: [self doAlignment].
aKey = $g ifTrue: [self group].
aKey = $G ifTrue: [self unGroup].!
yellowButtonMenu
^self drawPane controller initializeMenu! !
!SmallDraw methodsFor: 'clipboard'!
clipboardDisplayBox
^Clipboard inject: Clipboard first displayBox into: [:bb :o | bb merge: o displayBox]!
clipboardFull
^Clipboard notNil and: [Clipboard isEmpty not]!
copy
self hasSelection
ifTrue: [Clipboard := self selectedObjects
collect: [ :each | each deepCopy] ]!
cut
self hasSelection ifTrue: [
Clipboard := self selectedObjects.
self objects: (self objects reject: [:p | p value]).
self changed: #rectangle with: self clipboardDisplayBox]!
delete
self hasSelection ifTrue: [| cleanUp |
cleanUp := self selectedObjectsDisplayBox.
self objects: (self objects reject: [:p | p value]).
self changed: #rectangle with: cleanUp]!
duplicate
"Add a copy of the current selection without changing the Clipboard."
self hasSelection ifTrue: [ | newObjects |
newObjects := ((self selectedObjectAssociations
collect: [ :a | a copy key: a key deepCopy ]) do: [:oa |
oa key translateBy: self pasteOffset]).
self deselectAll.
self objects addAllFirst: newObjects.
self pages: (self pages max: self minimumObjectPages).
self changed: #rectangle with: self selectedObjectsDisplayBox]!
paste
self clipboardFull ifTrue: [
self deselectAll.
self objects addAllFirst: ((Clipboard do: [:o |
o translateBy: self pasteOffset]) copy collect: [:o | o -> true]).
self pages: (self pages max: self minimumObjectPages).
self changed: #rectangle with: self clipboardDisplayBox]!
pasteOffset
"Answer the default offset for pasting objects from their copied positions."
^10@10! !
!SmallDraw methodsFor: 'drawing'!
addFirst: anObject
"Add a new drawing object and select it."
| oldPages |
oldPages := self pages.
self deselectAll.
self objects addFirst: (anObject -> true).
self changed: #add with: (Array with: anObject) .
self pages: (self pages max: self minimumObjectPages)!
addObject: anObject
"Initialize the colors and line width of anObject and add it to the
display list."
anObject
insideColor: self insideColor;
borderColor: self borderColor;
lineWidth: self lineWidth.
self addFirst: anObject! !
!SmallDraw methodsFor: 'object attributes'!
borderColor
^borderColor!
changeBorderColor
| newColor names |
names := ColorValue constantNames asSortedCollection
asOrderedCollection addFirst: #NONE; yourself; asArray.
newColor := (PopUpMenu labelList: (Array with: names)) startUpWithHeading: 'Choose BORDER Color: '.
(newColor notNil and: [newColor > 0])
ifTrue: [newColor := newColor = 1
ifTrue: [nil]
ifFalse: [ColorValue perform: (names at: newColor)].
self hasSelection
ifFalse: [borderColor := newColor]
ifTrue: [self selectedObjects do: [:o | o borderColor: newColor].
self changed: #rectangle with: self selectedObjectsDisplayBox]]!
changeInsideColor
| newColor names |
names := ColorValue constantNames asSortedCollection
asOrderedCollection addFirst: #NONE; yourself; asArray.
newColor := (PopUpMenu labelList: (Array with: names))
startUpWithHeading: 'Choose INSIDE Color: '.
(newColor notNil and: [newColor > 0])
ifTrue: [newColor := newColor = 1
ifTrue: [nil]
ifFalse: [ColorValue perform: (names at: newColor)].
self hasSelection
ifFalse: [insideColor := newColor]
ifTrue: [self selectedObjects do: [:o | o insideColor: newColor].
self changed: #rectangle with: self selectedObjectsDisplayBox]]!
changeLineWidth
| answer |
answer := DialogView
request: 'New line width (in pixels)?'
initialAnswer: self lineWidth printString.
(answer isNil or: [answer isEmpty])
ifFalse: [answer := answer asNumber abs rounded max: 1.
self hasSelection
ifFalse: [lineWidth := answer]
ifTrue: [| bb |
bb := self selectedObjectsDisplayBox.
self selectedObjects do: [:o | o lineWidth: answer].
self changed: #rectangle
with: (bb merge: self selectedObjectsDisplayBox)]]!
insideColor
^insideColor!
lineWidth
^lineWidth! !
!SmallDraw methodsFor: 'object access'!
allObjects
^self objects collect: [:a | a key]!
allObjectsBoundingBox
"Answer the bounding that contains all selectedObjects."
^self allObjects
inject: (0@0 extent: 0@0)
into: [:bb :o | bb merge: o boundingBox]!
allObjectsDisplayBox
"Answer the bounding that contains all selectedObjects."
^self allObjects
inject: (0@0 extent: 0@0)
into: [:bb :o | bb merge: o displayBox]!
displayObjects
"Answer the receiver's objects in order for display purposes."
^self objects reverse collect: [:a | a key]!
objects
^objects!
objects: anOrderedCollectionOfAssociations
objects := anOrderedCollectionOfAssociations!
selectedObjectAssociations
"Answer all currently selected Associations."
^self objects select: [:p | p value]!
selectedObjects
"Answer all currently selected objects."
^self selectedObjectAssociations collect: [:a | a key]!
selectedObjectsBoundingBox
"Answer the bounding that contains all selectedObjects."
^self hasSelection
ifFalse: [nil]
ifTrue: [| allObjects |
(allObjects := self selectedObjects)
inject: allObjects first boundingBox
into: [:bb :o | bb merge: o boundingBox]]!
selectedObjectsDisplayBox
"Answer the display box that contains all currently selected objects."
| allObjects |
^(allObjects := self selectedObjects)
inject: allObjects first displayBox
into: [:bb :o | bb merge: o displayBox]! !
!SmallDraw methodsFor: 'selecting'!
deselectAll
| currentSelection |
currentSelection := self selectedObjects.
self objects do: [:p | p value: false].
self changed: #selection with: currentSelection!
hasSelection
"Answer true if there is at least one object selected."
^(self objects detect: [:p | p value] ifNone: [nil]) notNil!
selectAll
self deselectAll.
self objects do: [:p | p value: true].
self changed: #selection with: self selectedObjects!
selectObject: anObjectOrNil extend: aBoolean
self selectRange: (Array with: anObjectOrNil) extend: aBoolean!
selectRange: aCollectionOfObjects extend: aBoolean
"Toggle the selection status of the objects in aCollectionOfObjects. If
aBoolean is true then extend the selection."
aBoolean ifFalse: [self deselectAll].
aCollectionOfObjects do: [:obj | | oa |
(oa := self objects detect: [:o | o key == obj] ifNone: [nil]) notNil
ifTrue: [oa value: oa value not]].
self changed: #selection with: aCollectionOfObjects! !
!SmallDraw methodsFor: 'translate/scale'!
scaleBy: aPercentagePoint aboutHandleAt: anIndex absolute: aBoolean
"Scale all currently selected objects by aPercentagePoint from each
respective handle at anIndex, notify dependents of clean up region."
| cleanUp |
cleanUp := self selectedObjectsDisplayBox.
self selectedObjects do: [:o | o scaleBy: aPercentagePoint aboutHandleAt: anIndex absolute: aBoolean].
self pages: (self pages max: self minimumObjectPages).
self changed: #rectangle with: (cleanUp merge: self selectedObjectsDisplayBox)!
translateBy: aPoint
"Translate the selected objects by aPoint, notify dependents of clean up region."
| cleanUp |
cleanUp := self selectedObjectsDisplayBox.
self selectedObjects do: [:o | o translateBy: aPoint].
self pages: (self pages max: self minimumObjectPages).
self changed: #rectangle with: (cleanUp merge: (cleanUp translatedBy: aPoint)).! !
!SmallDraw methodsFor: 'grouping'!
group
"Group together the currently selected set of objects. Notify
dependents of the damaged rectangle because it may result in bringing objects
forward in the drawing."
self hasSelection ifTrue: [ |selection|
(selection := self selectedObjectAssociations) size > 1
ifTrue: [self objects
add: ((SDGraphicGroup with: self selectedObjects reverse) -> true)
before: selection first.
selection do: [:o | self objects remove: o].
self changed: #rectangle with: self selectedObjectsDisplayBox]]!
unGroup
"Break apart any currently selected GraphicGroups. Notify dependents of
a change in the current selection (does not require redrawing in rectangle)."
self hasSelection ifTrue: [ |selection|
selection := self selectedObjectAssociations.
self deselectAll.
"Break apart grouped object while ignoring non-groups."
selection do: [:g | (g key isKindOf: SDGraphicGroup)
ifTrue: [g key elements do: [:each | self objects add: (each -> true) after: g].
self objects remove: g]].
self changed: #selection with: self selectedObjects]! !
!SmallDraw methodsFor: 'bring/send'!
moveBackward
self hasSelection ifTrue: [
self selectedObjectAssociations reverseDo: [:oa | | after |
self objects last == oa
ifFalse: [after := self objects after: oa.
self objects remove: oa.
self objects add: oa after: after]].
self changed: #rectangle with: self selectedObjectsDisplayBox]!
moveForward
self hasSelection ifTrue: [
self selectedObjectAssociations do: [:oa | | before |
self objects first == oa
ifFalse: [before := self objects before: oa.
self objects remove: oa.
self objects add: oa before: before]].
self changed: #rectangle with: self selectedObjectsDisplayBox]!
moveToBack
self hasSelection ifTrue: [ | selection |
selection := self selectedObjectAssociations.
selection do: [:oa | self objects remove: oa].
self objects addAllLast: selection.
self changed: #rectangle with: self selectedObjectsDisplayBox]!
moveToFront
self hasSelection ifTrue: [ | selection |
selection := self selectedObjectAssociations.
selection do: [:oa | self objects remove: oa].
self objects addAllFirst: selection.
self changed: #rectangle with: self selectedObjectsDisplayBox]! !
!SmallDraw methodsFor: 'alignment'!
alignDialog
| dialog |
"create the dialog and its builder"
builder := (dialog := SimpleDialog new) builder.
builder aspectAt: #vAlign put: self vAlign.
builder aspectAt: #vAlignTo put: self vAlignTo.
builder aspectAt: #hAlign put: self hAlign.
builder aspectAt: #hAlignTo put: self hAlignTo.
"load the interface from the desired spec, then open it"
dialog allButOpenFrom: (self class interfaceSpecFor: #alignmentSpec).
builder openDialog.
(builder aspectAt: #accept) value
ifTrue: [
self vAlign value: (builder aspectAt: #vAlign) value.
self vAlignTo value: (builder aspectAt: #vAlignTo) value.
self hAlign value: (builder aspectAt: #hAlign) value.
self hAlignTo value: (builder aspectAt: #hAlignTo) value.
self doAlignment ]!
doAlignment
self hasSelection ifTrue: [| bb repair |
bb := self selectedObjectsBoundingBox.
repair := self selectedObjectsDisplayBox.
"Vertical movement."
self vAlign value ifTrue: [
self vAlignTo value == #top ifTrue:[
self selectedObjects do: [:o | o translateBy: 0@(bb origin y - o boundingBox origin y)]].
self vAlignTo value == #center ifTrue:[
self selectedObjects do: [:o | o translateBy: 0@(bb center y - o boundingBox center y)]].
self vAlignTo value == #bottom ifTrue:[
self selectedObjects do: [:o | o translateBy: 0@(bb corner y - o boundingBox corner y)]]].
"Horizontal movement."
self hAlign value ifTrue: [
self hAlignTo value == #left ifTrue:[
self selectedObjects do: [:o | o translateBy: (bb origin x - o boundingBox origin x) @0]].
self hAlignTo value == #center ifTrue:[
self selectedObjects do: [:o | o translateBy: (bb center x - o boundingBox center x) @0]].
self hAlignTo value == #right ifTrue:[
self selectedObjects do: [:o | o translateBy: (bb corner x - o boundingBox corner x) @0]]].
self changed: #rectangle with: repair]!
hAlign
"This method was generated by UIDefiner. The initialization provided
below may have been preempted by an initialize method."
^hAlign isNil ifTrue: [hAlign := false asValue] ifFalse: [hAlign]!
hAlignTo
"This method was generated by UIDefiner. The initialization provided
below may have been preempted by an initialize method."
^hAlignTo isNil ifTrue: [hAlignTo := nil asValue] ifFalse: [hAlignTo]!
vAlign
"This method was generated by UIDefiner. The initialization provided
below may have been preempted by an initialize method."
^vAlign isNil ifTrue: [vAlign := false asValue] ifFalse: [vAlign]!
vAlignTo
"This method was generated by UIDefiner. The initialization provided
below may have been preempted by an initialize method."
^vAlignTo isNil ifTrue: [vAlignTo := nil asValue] ifFalse: [vAlignTo]! !
!SmallDraw methodsFor: 'pages'!
bounds
^0@0 extent: self documentSize!
documentSize
"Answer the size of the document in terms of the number of 8.5 x 11
inch pages."
^self pages * self pageSizeInPixels!
minimumObjectPages
"Answer the minimum number of pages (as a Point) to hold the current collection of objects."
| minObjectPages minAcross minDown |
minObjectPages := self allObjectsDisplayBox extent.
minAcross := (minObjectPages x // self pageSizeInPixels x) +
((minObjectPages x \\ self pageSizeInPixels x) > 0
ifTrue: [1] ifFalse: [0]).
minDown := (minObjectPages y // self pageSizeInPixels y) +
((minObjectPages y \\ self pageSizeInPixels y) > 0
ifTrue: [1] ifFalse: [0]).
^self minimumPages max: (minAcross @ minDown)!
minimumPages
"The minimum size for a document is one page."
^1@1!
pages
"Answer the number of pages arranged horizontally and vertically (as a Point)."
^pages!
pages: aPoint
"Set the size of the document in terms of the number of pages across
and down. The minimum allowed is the minimum size that contains all objects."
| newPages |
newPages := aPoint max: self minimumObjectPages.
pages = newPages
ifFalse: [
pages := newPages.
self drawPane changedPreferredBounds: nil.
self changed: #pages]!
pagesAcross
"Answer the number of pages arranged horizontally."
^self pages x!
pagesDown
"Answer the number of pages arranged vertically."
^self pages y!
pageSizeInPixels
"Answer the size of one 8.5 x 11 inch page (with 1/2 inch margins),
scaled by the number of pixels per inch (72). This number is calculated as:
((7.5@10) * 72) rounded."
^540@720!
preferredBounds
^self bounds!
setSmallestPages
self pages: self minimumObjectPages! !
"-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- "!
SmallDraw class
instanceVariableNames: ''
"
The following instance variables are inherited by this class:
ApplicationModel class -
Model class -
Object class -
Class -- name, classPool, sharedPools
ClassDescription -- instanceVariables, organization
Behavior -- superclass, methodDict, format, subclasses
Object --
"!
!SmallDraw class methodsFor: 'instance creation'!
new
"SmallDraw open"
^super new initialize! !
!SmallDraw class methodsFor: 'interface specs'!
alignmentSpec
"UIPainter new openOnClass: self andSelector: #alignmentSpec"
^#(#FullSpec #window: #(#WindowSpec #label: 'Align Selected Objects' #min: #(#Point 50 50 ) #bounds: #(#Rectangle 897 349 1191 608 ) ) #component: #(#SpecCollection #collection: #(#(#RadioButtonSpec #layout: #(#Point 40 64 ) #model: #vAlignTo #label: 'Top' #select: #top ) #(#RadioButtonSpec #layout: #(#Point 40 112 ) #model: #vAlignTo #label: 'Center' #select: #center ) #(#RadioButtonSpec #layout: #(#Point 40 160 ) #model: #vAlignTo #label: 'Bottom' #select: #bottom ) #(#RadioButtonSpec #layout: #(#Point 176 64 ) #model: #hAlignTo #label: 'Left' #select: #left ) #(#RadioButtonSpec #layout: #(#Point 176 112 ) #model: #hAlignTo #label: 'Center' #select: #center ) #(#RadioButtonSpec #layout: #(#Point 176 160 ) #model: #hAlignTo #label: 'Right' #select: #right ) #(#GroupBoxSpec #layout: #(#Rectangle 32 56 128 192 ) ) #(#GroupBoxSpec #layout: #(#Rectangle 168 56 264 192 ) ) #(#ActionButtonSpec #layout: #(#Rectangle 48 216 120 240 ) #model: #accept #label: 'Okay' #defaultable: tr!
ue ) #(#ActionButtonSpec #layout:
#(#Rectangle 176 216 248 240 ) #model: #cancel #label: 'Cancel' #defaultable: true ) #(#CheckBoxSpec #layout: #(#Point 32 24 ) #name: #vAlign #model: #vAlign #label: 'Vertical' ) #(#CheckBoxSpec #layout: #(#Point 168 24 ) #name: #hAlign #model: #hAlign #label: 'Horizontal' ) ) ) )!
windowSpec
"UIPainter new openOnClass: self andSelector: #windowSpec"
^#(#FullSpec #window: #(#WindowSpec #label: 'SmallDraw' #min: #(#Point 50 50 ) #bounds: #(#Rectangle 805 415 1268 840 ) #flags: 4 #menu: #yellowButtonMenu ) #component: #(#SpecCollection #collection: #(#(#ArbitraryComponentSpec #layout: #(#LayoutFrame 0 0 27 0 0 1 0 1 ) #flags: 11 #component: #drawPane ) #(#LabelSpec #layout: #(#LayoutOrigin -125 1 2 0 ) #label: 'Scale: ' ) #(#InputFieldSpec #layout: #(#LayoutFrame -76 1 1 0 -1 1 25 0 ) #model: #scalePane #tabable: false #isReadOnly: true #type: #string ) ) ) )! !
View subclass: #SmallDrawView
instanceVariableNames: 'scale useGrid showPageBreaks '
classVariableNames: 'DrawHandle '
poolDictionaries: ''
category: 'SmallDraw'
"
The following instance variables are inherited by this class:
View -- controller
DependentPart -- model
VisualPart -- container
VisualComponent -
Object --
"!
SmallDrawView comment:
'I represent a scalable graphics view that asks its model''s displayObjects to
display themselves on my graphicsContext at a given scale, which I keep track
of. I allow my scale to be changed through a menu selection. One of several
scales may be selected, from minScale (12.5%) to maxScale (3200%). I am
designed to be used with the model SmallDraw and controller
SmallDrawController.
I keep track of my current scale as a percent. When I ask objects to display
themselves, I convert this value into a scaling factor Point.
I refresh myself in several ways whenever I get an update: message. The
following aspects are monitored:
Aspect: #add - indicates that a new display object was added to the list
Action: redisplay objects and toggle the handles for the newly added object
Aspect: #selection - indicates that the selection has been changed
Action: toggle handles for the selected objects
Aspect: #rectangle - indicates that a rectangle needs repairing
Action: redraw only the rectangle
Aspect: #pages - indicates that the document has changed size
Action: update my scrolling controls and redraw the entire view
Aspect: #zoomIn or #zoomOut - generated by the model via the menu bar
Action: change the scale (multiply or divide) by one step (factor of 2)
When objects are selected I ask them for their display handle points and then I
display a black rectangle at each of the points. I use a BITBLT technique so
that the handles can be drawn and erased quickly without having to refresh the
drawing.
I can be scrolled both vertically and horizontally. I use a grid of either the
minimum (1@1) or whatever my model''s pasteOffset is set to. This can be
changed through a menu selection. I also am able to display page breaks,
depending on the size of document of my model. Page breaks show up as light
gray rectangles and may be turned on or off through a menu selection. I obtain
the size of the pages and the page configuration from my model.
I have the following instance variables:
scale <Number> current scale to be used for
display (as percent)
useGrid <Boolean> indicates whether to use a fine/coarse grid
showPageBreaks <Boolean> indicates whether page breaks should be
displayed
I have one class variable:
DrawHandle <Rectangle> display handle for graphic object manipulation'!
!SmallDrawView methodsFor: 'displaying'!
displayObjects: aCollectionOfObjects on: aGC
| bb|
aGC translateBy: self offset.
bb := aGC clippingBounds scaledBy: self displayScale reciprocal.
aCollectionOfObjects do: [:o |
(o displayBox intersects: bb)
ifTrue: [o displayOn: aGC scale: self displayScale]].
aGC translateBy: self offset negated!
displayOn: aGC
self
displayPageBreaksOn: aGC;
displayObjects: self model displayObjects on: aGC;
toggleHandlesFor: self model selectedObjects on: aGC.!
displayPageBreaksOn: aGC
self showPageBreaks
ifTrue: [ | page |
aGC translateBy: self offset.
page := (self model pageSizeInPixels * self displayScale) rounded.
aGC paint: self pageBreakColor.
0 to: self model pagesAcross - 1 do: [:x |
0 to: self model pagesDown - 1 do: [:y |
aGC
displayRectangularBorder: (0@0 extent: page)
at: (page * (x@y))]] .
aGC translateBy: self offset negated]!
handle
^DrawHandle scaledBy: self displayScale reciprocal!
preferredBounds
"This tells the scrollbars how large the current document is,
so they can update themselves."
^self model bounds scaledBy: scale / 100!
toggleHandlesFor: aCollectionOfObjects on: aGC
aGC translateBy: self offset.
aCollectionOfObjects do: [:o |
o displayHandles do: [:h || rect image |
rect := DrawHandle translatedBy: h * self displayScale.
(aGC clippingBounds intersects: rect)
ifTrue: [rect := ((rect intersect: aGC clippingBounds)
translatedBy: aGC translation) rounded.
(image := (aGC medium contentsOfArea: rect) first)
copy: (0 @ 0 extent: rect extent)
from: 0 @ 0
in: image
rule: 10;
displayOn: aGC
at: rect origin - aGC translation]]].
aGC translateBy: self offset negated! !
!SmallDrawView methodsFor: 'scaling'!
defaultScale
^100!
displayScale
"Answer the screen scale factor, calculated from the percentage."
^(self scale / 100) asPoint!
maxScale
^3200!
minScale
^25/2!
scale
^scale!
scale100
self scale: 100!
scale12
self scale: self minScale!
scale1600
self scale: 1600!
scale200
self scale: 200!
scale25
self scale: 25!
scale3200
self scale: self maxScale!
scale400
self scale: 400!
scale50
self scale: 50!
scale800
self scale: 800!
scale: aNumber
scale := (aNumber abs min: self maxScale) max: self minScale.
self model scalePane: scale.
"self setToTop."
"self updateMarker."
self invalidateRectangle: self bounds repairNow: true.
self changedPreferredBounds: nil.!
zoomIn
self scale: (self scale * 2)!
zoomOut
self scale: (self scale / 2)! !
!SmallDrawView methodsFor: 'grid'!
gridOff
"Turn the grid off."
useGrid := false.
controller updateMenu.!
gridOn
"Turn the grid on."
useGrid := true.
controller updateMenu.!
scrollGrid
"Answer the receiver's grid for scrolling."
^self useGrid
ifTrue: [self model pasteOffset]
ifFalse: [1@1]!
toggleGrid
"Turn the grid on or off."
useGrid := useGrid not.
controller updateMenu.!
useGrid
"Answer whether the grid is currently on."
^useGrid! !
!SmallDrawView methodsFor: 'page breaks'!
pageBoundary
^0@0 extent: self dataExtent * self displayScale!
pageBreakColor
^ColorValue gray!
showPageBreaks
^showPageBreaks!
togglePageBreaks
showPageBreaks := self showPageBreaks not.
self update: #pages! !
!SmallDrawView methodsFor: 'updating'!
repairRectangle: aRectangle
self
invalidateRectangle: (((aRectangle scaledBy: self displayScale) rounded
expandedBy: (DrawHandle extent / 2) rounded) translatedBy: self offset)
repairNow: true!
update: anAspectSymbol with: anObject
container isNil ifTrue: [^self]. "too soon to do this"
#add = anAspectSymbol
ifTrue: [self displayObjects: anObject on: self graphicsContext;
toggleHandlesFor: anObject on: self graphicsContext].
#selection == anAspectSymbol
ifTrue: [ self toggleHandlesFor: anObject on: self graphicsContext].
#rectangle == anAspectSymbol
ifTrue: [self repairRectangle: anObject].
#pages == anAspectSymbol
ifTrue: [self invalidate].
#zoomIn == anAspectSymbol
ifTrue: [self zoomIn. self invalidate].
#zoomOut == anAspectSymbol
ifTrue: [self zoomOut. self invalidate].! !
!SmallDrawView methodsFor: 'private'!
model: aModel
super model: aModel.
useGrid := true.
scale := self defaultScale.
showPageBreaks := true.!
offset
"patch to SmallDraw, 6-9-93 dka"
^-1@-1! !
"-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- "!
SmallDrawView class
instanceVariableNames: ''
"
The following instance variables are inherited by this class:
View class -
DependentPart class -
VisualPart class -
VisualComponent class -
Object class -
Class -- name, classPool, sharedPools
ClassDescription -- instanceVariables, organization
Behavior -- superclass, methodDict, format, subclasses
Object --
"!
!SmallDrawView class methodsFor: 'handles'!
initialize
"Initialize the size of the handles with a Rectangle centered at the origin."
DrawHandle := (3@3) negated corner: (3@3).! !
ControllerWithMenu subclass: #SmallDrawController
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'SmallDraw'
"
The following instance variables are inherited by this class:
ControllerWithMenu -- menuHolder, performer
Controller -- model, view, sensor
Object --
"!
SmallDrawController comment:
'I represent a controller that is able to accept mouse clicks to draw various
graphics objects in my view. I use rubber-banding of lines to provide
interactive feedback. Look at my creation methods. I am designed to be used
with the model SmallDraw and view SmallDrawView.
When the Menu Bar or the yellowButton is pressed, the PopUpMenu defined in
my yellowButtonMenu method is displayed. Parts of this menu are to be carried
out by the model (SmallDraw), the view (SmallDrawView) or the controller (me).
Depending on user requests, the yellowButtonMenu may be updated dynamically.
When the redButton is pressed, I check to see whether the mouse touches any
objects or whether I should rubber-band a rectangle to select a group of
objects. If the mouse touches any objects, I either tell my model to select the
object or I modify it in some way. If the mouse is on a handle point I scale
the object, otherwise I translate it.
I reimplement methods that obtain mouse points from my input sensor because I
want points to be fixed to my view''s displayGrid. If the shift button is down,
I amplify the grid by a scaling factor.'!
!SmallDrawController methodsFor: 'accessing'!
gridMagnification
"Answer the scale factor for the grid when the shift key is pressed."
^4!
rubberBandDelay
"Answer the number of milliseconds for displaying rubberbanded shapes."
^25!
rubberBandLineWidth
^2! !
!SmallDrawController methodsFor: 'drawing'!
addNewEllipse
"Obtain a sequence of mouse clicks from the user that define a rectangle.
Then ask the receiver's model to add a new ellipse based on this rectangle
to its collection of objects."
| r savedCursor |
savedCursor := Cursor currentCursor.
Cursor currentCursor: Cursor origin.
[self sensor anyButtonPressed] whileFalse.
Cursor currentCursor: Cursor corner.
r := self rectangleFromScreen.
Cursor currentCursor: savedCursor.
self model addObject: (SDEllipse origin: r origin corner: r corner)!
addNewLine
"Obtain two mouse clicks from the user that define a line segment. Then
ask the receiver's model to add this new line to its collection of objects."
| firstPoint aLine screen origin savedCursor lastPoint |
savedCursor := Cursor currentCursor.
Cursor currentCursor: Cursor crossHair.
screen := Screen default.
origin := self sensor globalOrigin.
firstPoint := lastPoint := self waitButton.
aLine := Array with: firstPoint with: lastPoint.
[self sensor anyButtonPressed]
whileTrue:
[screen
displayShape: aLine
lineWidth: self model lineWidth
at: origin
forMilliseconds: self rubberBandDelay.
aLine at: 2 put: self cursorPoint].
Cursor currentCursor: savedCursor.
self model addObject: (SDLineSegment start: (aLine at: 1)
- self view offset / self view displayScale end: (aLine at: 2)
- self view offset / self view displayScale)!
addNewPolygon
"Obtain a sequence of mouse clicks from the user that define a polygon.
Then ask the receiver's model to add this new polygon to its collection of objects."
| firstPoint midPoint endPoint doubleClick aPolygon screen origin savedCursor |
savedCursor := Cursor currentCursor.
Cursor currentCursor: Cursor crossHair.
screen := Screen default.
origin := self sensor globalOrigin.
"Get the first click point."
endPoint := firstPoint := midPoint := self waitClickButton.
aPolygon := OrderedCollection with: firstPoint.
doubleClick := false.
"Get a polygon, one click at a time."
[doubleClick] whileFalse: [
"Wait for a mouse click"
[self sensor anyButtonPressed] whileFalse: [
screen
displayShape: (Array
with: aPolygon last
with: (endPoint := self cursorPoint)
with: aPolygon first)
lineWidth: self model lineWidth
at: origin
forMilliseconds: self rubberBandDelay].
"Now wait for them to let go."
[self sensor anyButtonPressed] whileTrue: [
screen
displayShape: (Array with: aPolygon last with: endPoint)
lineWidth: self model lineWidth
at: origin
forMilliseconds: self rubberBandDelay].
"Draw the last segment obtained."
self view graphicsContext
lineWidth: self model lineWidth;
displayLineFrom: aPolygon last to: endPoint.
(doubleClick := (midPoint dist: endPoint) < 5)
ifFalse: [aPolygon addLast: endPoint].
midPoint := endPoint].
Cursor currentCursor: savedCursor.
"Close the polygon."
aPolygon addLast: aPolygon first.
self model addObject: (SDPolygon vertices: (aPolygon collect: [:pt |
(pt - self view offset / self view displayScale) rounded]))!
addNewPolyline
"Obtain a sequence of mouse clicks from the user that define a
polyline. Then ask the receiver's model to add this new polyline to its
collection of objects."
| firstPoint midPoint endPoint doubleClick aPolyline screen origin savedCursor |
savedCursor := Cursor currentCursor.
Cursor currentCursor: Cursor crossHair.
screen := Screen default.
origin := self sensor globalOrigin.
"Get the first click point."
firstPoint := midPoint := self waitClickButton.
aPolyline := OrderedCollection with: firstPoint.
doubleClick := false.
"Get a polyline, one click at a time."
[doubleClick] whileFalse: [
"Wait for a mouse click"
[self sensor anyButtonPressed] whileFalse: [
screen
displayShape: (Array with: aPolyline last with: (endPoint := self cursorPoint))
lineWidth: self model lineWidth
at: origin
forMilliseconds: self rubberBandDelay].
"Now wait for them to let go."
[self sensor anyButtonPressed] whileTrue: [
screen
displayShape: (Array with: aPolyline last with: endPoint)
lineWidth: self model lineWidth
at: origin
forMilliseconds: self rubberBandDelay].
"Draw the last segment obtained."
self view graphicsContext
lineWidth: self model lineWidth;
displayLineFrom: aPolyline last to: endPoint.
(doubleClick := (midPoint dist: endPoint) < 5)
ifFalse: [aPolyline addLast: endPoint].
midPoint := endPoint].
Cursor currentCursor: savedCursor.
self model addObject: (SDPolyline vertices: (aPolyline collect: [:pt |
(pt - self view offset / self view displayScale) rounded]))!
addNewQuadrangle
"Obtain a sequence of mouse clicks from the user that define a rectangle.
Then ask the receiver's model to add this new rectangle to its collection
of objects."
| r savedCursor |
savedCursor := Cursor currentCursor.
Cursor currentCursor: Cursor origin.
[self sensor anyButtonPressed] whileFalse.
Cursor currentCursor: Cursor corner.
r := self rectangleFromScreen.
Cursor currentCursor: savedCursor.
self model addObject: (SDQuadrangle origin: r origin corner: r corner)!
cornerOfRectangleFromScreenWithOrigin: aPoint
"Answer the resulting corner of the rectangle with origin at aPoint
obtained from the user in the view's coordinate system. Assume the mouse is
already pressed."
| origin rectangle polygon screen lastPoint newPoint |
screen := Screen default.
lastPoint := self cursorPoint.
origin := self sensor globalOrigin.
rectangle := Rectangle origin: aPoint corner: lastPoint.
polygon := (Array new: 5)
at: 1 put: rectangle topLeft;
at: 2 put: rectangle topRight;
at: 3 put: rectangle bottomRight;
at: 4 put: rectangle bottomLeft;
at: 5 put: rectangle topLeft;
yourself.
[self sensor anyButtonPressed]
whileTrue:
[screen
displayShape: polygon
lineWidth: self rubberBandLineWidth
at: origin
forMilliseconds: self rubberBandDelay.
(newPoint := self cursorPoint) = lastPoint
ifFalse:
[rectangle := (Rectangle vertex: aPoint
vertex: (lastPoint := newPoint)) rounded.
polygon
at: 1 put: rectangle topLeft;
at: 2 put: rectangle topRight;
at: 3 put: rectangle bottomRight;
at: 4 put: rectangle bottomLeft;
at: 5 put: rectangle topLeft]].
^lastPoint - self view offset!
endOfLineFromScreenWithOrigin: aPoint
"Answer the resulting end of the line with one end at aPoint obtained
from the user in the view's coordinate system. Assume the mouse is already
pressed."
| origin screen line |
screen := Screen default.
origin := self sensor globalOrigin.
line := Array with: aPoint with: self cursorPoint.
[self sensor anyButtonPressed]
whileTrue:
[screen
displayShape: line
lineWidth: self rubberBandLineWidth
at: origin
forMilliseconds: self rubberBandDelay.
self cursorPoint = (line at: 2)
ifFalse:
[line at: 2 put: self cursorPoint]].
^(line at: 2) - self view offset!
rectangleFromScreen
"Answer the resulting rectangle obtained from the user in the view's
coordinate system. Assume the mouse is already pressed."
| origin rectangle polygon screen lastPoint start newPoint |
screen := Screen default.
start := lastPoint := self cursorPoint.
origin := self sensor globalOrigin.
rectangle := Rectangle origin: start corner: lastPoint.
polygon := Array new: 5 withAll: start.
[self sensor anyButtonPressed]
whileTrue:
[screen
displayShape: polygon
lineWidth: self rubberBandLineWidth
at: origin
forMilliseconds: self rubberBandDelay.
(newPoint := self cursorPoint) = lastPoint
ifFalse:
[rectangle := Rectangle vertex: start
vertex: (lastPoint := newPoint).
polygon
at: 1 put: rectangle topLeft;
at: 2 put: rectangle topRight;
at: 3 put: rectangle bottomRight;
at: 4 put: rectangle bottomLeft;
at: 5 put: rectangle topLeft]].
^(rectangle moveBy: self view offset negated)
scaledBy: self view displayScale reciprocal! !
!SmallDrawController methodsFor: 'interaction'!
clickOnObject: anObject at: aModelPoint
"The user is clicking on anObject at aModelPoint. If anObject is
already selected we either de-select it or modify it. If it's not already
selected we select it (possibly extending the selection), wait momentarily, and
then if the mouse remains pressed we translate it. The pause is added so that
objects won't get moved accidently."
(self model selectedObjects includes: anObject)
ifTrue: [self sensor shiftDown
ifTrue: [self model selectObject: anObject extend: true]
ifFalse: [self modifyObject: anObject at: aModelPoint]]
ifFalse: [self model selectObject: anObject extend: self sensor shiftDown.
(Delay forMilliseconds: 20) wait.
self sensor anyButtonPressed
ifTrue: [self translateObject: anObject]]!
modifyObject: anObject at: aModelPoint
"If the cursorPoint is on a handle point of anObject we scale it,
otherwise we translate it."
| handle |
(handle := anObject displayHandles
detect: [:h | self view handle containsPoint: aModelPoint - h]
ifNone: [nil]) notNil
ifTrue: [self scaleObject: anObject usingHandle: handle]
ifFalse: [self translateObject: anObject]!
objectHitBy: aModelPoint
"Answer the single object that is touched by aModelPoint. Selected
objects have preference if one of their handles is touched. Otherwise answer
the first object hit, or nil."
self model selectedObjects do: [:each |
each displayHandles do: [:pt |
(self view handle containsPoint: aModelPoint - pt)
ifTrue: [^each]]].
self model allObjects do: [:each |
(each containsPoint: aModelPoint)
ifTrue: [^each]].
^nil!
objectsInRectangle: aRectangle
"Determine the objects contained in aRectangle."
^self model allObjects select: [:o | o insideRectangle: aRectangle]!
scaleObject: anObject usingHandle: aHandlePoint
"The cursor is over the handle at aHandlePoint. The opposite handle
point should remain stationary (anchorHandle) as the cursor is moved."
|scale anchorHandle newPoint scaleFactor bbox testObject |
scale := self view displayScale.
anchorHandle := anObject handleOpposite: aHandlePoint.
newPoint := (anObject animateUsingRectangle
ifTrue: [self cornerOfRectangleFromScreenWithOrigin:
(anchorHandle * scale) + self view offset]
ifFalse: [self endOfLineFromScreenWithOrigin:
(anchorHandle * scale) + self view offset]) / scale.
"NOTE: If the object being scaled isSquished (either vertical or horizontal),
it will have a zero in its original vector diagonal so scaling should be done
in absolute terms and the percentage is calculated from the resulting
unitVector multiplied by the ratio of the newDiagonalDistance over the
oldDiagonalDistance. Absolute scaling should only be performed by other
'squished' objects.
If the object being scaled is not squished there is no danger of
division by zero so the percentage is calculated from the ratio of the
newDiagonalVector divided by the oldDiagonalVector."
scaleFactor := anObject isSquished
ifTrue: [(newPoint - anchorHandle) unitVector *
((newPoint dist: anchorHandle) /
(aHandlePoint dist: anchorHandle))]
ifFalse: [(newPoint - anchorHandle) /
(aHandlePoint - anchorHandle)].
"In bounds check."
bbox := self model selectedObjectsDisplayBox.
testObject := SDQuadrangle origin: bbox origin corner: bbox corner.
testObject
scaleBy: scaleFactor
aboutHandleAt: (anObject indexOfHandle: anchorHandle)
absolute: anObject isSquished.
(testObject origin x < 0 or: [testObject origin y < 0])
ifFalse: [self model
scaleBy: scaleFactor
aboutHandleAt: (anObject indexOfHandle: anchorHandle)
absolute: anObject isSquished]
ifTrue: [Screen default ringBell]!
translateObject: anObject
| screen end start scale outline bbox bb origin newPoint |
screen := Screen default.
start := end := self cursorPoint.
scale := self view displayScale.
"Display the outline of the first object in the collection and the boundingBox
of the entire collection of objects (if applicable)."
outline := anObject outline collect: [:pt | (pt * scale) rounded].
bbox := (self model selectedObjectsDisplayBox scaledBy: scale) rounded.
bb := (self model selectedObjects size > 1
ifTrue: [(Array new: 5)
at: 1 put: bbox origin;
at: 2 put: bbox topRight;
at: 3 put: bbox corner;
at: 4 put: bbox bottomLeft;
at: 5 put: bbox origin;
yourself]
ifFalse: [nil]).
origin := self sensor globalOrigin + self view offset - start.
[self sensor anyButtonPressed]
whileTrue: [screen
displayShape: outline
lineWidth: self rubberBandLineWidth
at: origin + end
forMilliseconds: self rubberBandDelay;
displayShape: bb
lineWidth: self rubberBandLineWidth
at: origin + end
forMilliseconds: self rubberBandDelay.
end := self cursorPoint].
newPoint := (bbox translatedBy: (end - start)) origin.
(newPoint x < 0 or: [newPoint y < 0])
ifFalse: [self model translateBy: (end - start) / scale]
ifTrue: [Screen default ringBell]! !
!SmallDrawController methodsFor: 'menu'!
initializeMenu
"Define the initial yellowButtonMenu. This will be changed at runtime
according to the grid on|off state in the view."
^menuHolder := (ValueHolder with: self yellowButtonMenu)!
menuMessageReceiver
^model!
updateMenu
self menuHolder value: self yellowButtonMenu!
yellowButtonMenu
"Define and answer the yellowButtonMenu.
This menu defines actions that are to be carried out by either the
model (SmallDraw), view (SmallDrawView) or controller (SmallDrawController).
The different means of defining actions and receivers are noted below.
The current version of this method assumes there is an instance method
SmallDrawController>>menuMessageReceiver, which answers the model."
| mb |
mb := MenuBuilder new.
mb
beginSubMenuLabeled: 'Edit';
"note: these actions will be sent to the controller's menuMessageReceiver"
add: 'copy'->#copy;
add: 'cut'->#cut;
add: 'paste'->#paste;
line;
add: 'select all'->#selectAll;
add: 'duplicate'->#duplicate;
endSubMenu;
line;
beginSubMenuLabeled: 'Draw';
beginSubMenuLabeled: 'new';
"note: these actions will be executed by evaluating the blocks"
add: 'line'->[:c | self addNewLine];
add: 'polyline'->[:c | self addNewPolyline];
add: 'rectangle'->[:c | self addNewQuadrangle];
add: 'polygon'->[:c | self addNewPolygon];
add: 'ellipse'->[:c | self addNewEllipse];
endSubMenu;
line;
add: 'inside color...'->#changeInsideColor;
add: 'border color...'->#changeBorderColor;
add: 'line width...'->#changeLineWidth;
line;
beginSubMenuLabeled: 'bring';
add: 'forward'->#moveForward;
add: 'to front'->#moveToFront;
endSubMenu;
beginSubMenuLabeled: 'send';
add: 'backward'->#moveBackward;
add: 'to back'->#moveToBack;
endSubMenu;
line;
add: 'group'->#group;
add: 'ungroup'->#unGroup;
add: 'align...'->#alignDialog;
endSubMenu;
line;
beginSubMenuLabeled: 'View';
"note: these actions are carried out by the SmallDrawView"
add: 'refresh'->[:c | view invalidate];
line;
add: 'zoom in'->[:c | view zoomIn];
add: 'zoom out'->[:c | view zoomOut];
line;
add: 'grid ', "during initialization, 'view' has not been set yet"
(view isNil ifTrue: ['off'] ifFalse: [
(view useGrid ifTrue: [ 'off' ] ifFalse: [ 'on' ])
]) ->[:c | view toggleGrid];
line;
"note: this action is carried out by the SmallDraw object itself"
add: 'reset pages'->[:c | model setSmallestPages];
endSubMenu.
^mb menu.! !
!SmallDrawController methodsFor: 'sensor access'!
cursorPoint
"Answer the current cursor point, fixing it to the grid. If the shift
key is pressed the grid is magnified so that it's easier to constrain movements
along either axis."
^self sensor shiftDown
ifTrue: [self sensor cursorPoint grid: self view scrollGrid * self gridMagnification]
ifFalse: [self sensor cursorPoint grid: self view scrollGrid]!
processKeyboard
"Determine whether the user pressed the keyboard. If so, read the key and pass it on to the model."
self sensor keyboardPressed
ifTrue: [ | keyHit |
keyHit := self sensor keyboardEvent keyValue.
"Check for backspace here."
keyHit = Character backspace
ifTrue: [self model processCommandKey: keyHit].
(self sensor altDown or: [self sensor metaDown])
ifTrue: [
"KeyValues are lowercase so we must convert to uppercase if the shift key is down."
self sensor shiftDown ifTrue: [keyHit := keyHit asUppercase].
self model processCommandKey: keyHit]]!
waitButton
"Wait for the user to press any mouse button and then answer with the
current location of the cursor fixed to the grid. If the shift key is pressed
the grid is magnified so that it's easier to constrain movements along either
axis."
^self sensor shiftDown
ifTrue: [self sensor waitButton grid: self view scrollGrid * self gridMagnification]
ifFalse: [self sensor waitButton grid: self view scrollGrid]!
waitClickButton
"Wait for the user to click (press and then release) any mouse button
and then answer with the current location of the cursor fixed to the grid. If
the shift key is pressed the grid is magnified so that it's easier to constrain
movements along either axis."
^self sensor shiftDown
ifTrue: [self sensor waitClickButton grid: self view scrollGrid * self gridMagnification]
ifFalse: [self sensor waitClickButton grid: self view scrollGrid]! !
!SmallDrawController methodsFor: 'control'!
controlActivity
"First check the keyboard and then do the usual."
self processKeyboard.
super controlActivity.!
redButtonActivity
"Process either a single object selection or range selection in a
rectangle."
| hitObject modelPoint|
"NOTE: the first point obtained should not be fixed to the grid. Otherwise it
may be impossible to select some objects by their edge!!"
modelPoint := ((self sensor cursorPoint - self view offset) / self view displayScale) rounded.
(hitObject := self objectHitBy: modelPoint) notNil
ifTrue: [self clickOnObject: hitObject at: modelPoint]
ifFalse: [self model selectRange: (self objectsInRectangle: self rectangleFromScreen)
extend: self sensor shiftDown]! !
Object subclass: #SDGraphicObject
instanceVariableNames: 'insideColor borderColor lineWidth handles boundingBox '
classVariableNames: ''
poolDictionaries: ''
category: 'SmallDraw'
"
The following instance variables are inherited by this class:
Object --
"!
SDGraphicObject comment:
'I represent the abstract superclass of all SmallDraw graphic objects. I define
instance variables for visual attributes used when displaying my instances and
I have methods for accessing and setting these attributes.
I am also able to translate and scale myself. Of course, this must be done by
concrete subclasses.
I keep track of my handle points that are used to indicate that I am selected
and for manipulating myself.
Concrete subclasses are responsible for displaying themselves at a particular
scale using the method:
displayOn: aGraphicsContext scale: aScalePoint
In addition, I implement a method deepCopy to specify how to properly create
a copy of a graphic object and its instance variables, for use with the copy/paste
clipboard. Subclasses which add instance variables should override this method
to extend it as needed (see SDGraphicGroup>>deepCopy and other examples).
I have the following instance variables:
insideColor <ColorValue> the inside color
borderColor <ColorValue> the border color
lineWidth <Integer> the line width
handles <Array> four points used for manipulating objects
boundingBox <Rectangle> minimum bounding rectangle of object'!
!SDGraphicObject methodsFor: 'accessing'!
borderColor
^borderColor!
borderColor: aColorValue
borderColor := aColorValue!
boundingBox
boundingBox isNil
ifTrue: [self computeBoundingBox].
^boundingBox!
center
^self boundingBox center!
defaultLineWidth
^1!
displayBox
^self boundingBox expandedBy: (1 max: (self lineWidth/2) truncated) asPoint!
displayHandles
"Answer the handles to use for displaying the receiver."
^self isSquished
ifTrue: [Array with: self boundingBox origin with: self boundingBox corner]
ifFalse: [self handles]!
handleOpposite: aHandlePoint
"Answer the handle point that is the opposite corner of aHandlePoint."
(self handles at: 3) = aHandlePoint
ifTrue: [^self handles at: 1].
(self handles at: 4) = aHandlePoint
ifTrue: [^self handles at: 2].
(self handles at: 1) = aHandlePoint
ifTrue: [^self handles at: 3].
(self handles at: 2) = aHandlePoint
ifTrue: [^self handles at: 4].!
handles
^handles!
height
^self boundingBox height!
indexOfHandle: aHandlePoint
"Answer the handle point that is the opposite corner of aHandlePoint."
1 to: self handles size do: [:i | (self handles at: i) = aHandlePoint ifTrue: [^i]].
self error: 'Something went wrong.'!
insideColor
^insideColor!
insideColor: aColorValue
insideColor := aColorValue!
lineWidth
lineWidth isNil
ifTrue: [lineWidth := self defaultLineWidth].
^lineWidth!
lineWidth: anInteger
lineWidth := anInteger rounded.
self computeBoundingBox!
origin
^self boundingBox origin!
width
^self boundingBox width! !
!SDGraphicObject methodsFor: 'displaying'!
displayOn: aGC scale: aScalePoint
self subclassResponsibility!
joinStyle
"Answer the appropriate join style for displaying the receiver."
^GraphicsContext joinBevel! !
!SDGraphicObject methodsFor: 'converting'!
outline
"Answer an array of 5 points representing a closed polygon of the
receiver's 4 handle points."
^(Array new: 5)
at: 1 put: (self handles at: 1);
at: 2 put: (self handles at: 2);
at: 3 put: (self handles at: 3);
at: 4 put: (self handles at: 4);
at: 5 put: (self handles at: 1);
yourself! !
!SDGraphicObject methodsFor: 'manipulation'!
scaleAbsoluteBy: aPercentagePoint aboutHandleAt: anIndex
"aPercentagePoint is calculated as (unitVector) * (newDiagonalDistance
/ oldDiagonalDistance). This is appropriate when the object being scaled was
also squished."
self subclassResponsibility!
scaleBy: aPercentagePoint aboutHandleAt: anIndex absolute: aBoolean
"aPercentagePoint is specified in two different ways according to aBoolean.
If aBoolean is true, the object being scaled was squished
(either vertical or horizontal) which means that it will have a zero in its
original diagonal vector so scaling should be done in absolute terms and
aPercentagePoint is specified by the resulting unitVector multiplied by the
ratio of the newDiagonalDistance over the oldDiagonalDistance. Absolute scaling
is only performed by the receiver if the receiver is also squished.
If aBoolean is false there is no danger of division by zero so
aPercentagePoint is specified by the ratio of newDiagonalVector / oldDiagonalVector.
All scaling is done relative to the receiver's handle point at anIndex."
aBoolean
ifTrue: [self isSquished
ifTrue: [self scaleAbsoluteBy: aPercentagePoint aboutHandleAt: anIndex]]
ifFalse: [self scaleRelativeBy: aPercentagePoint aboutHandleAt: anIndex]!
scaleRelativeBy: aPercentagePoint aboutHandleAt: anIndex
"aPercentagePoint is calculated as (newDiagonalVector /
oldDiagonalVector). The receiver should be scaled proportionally to the ratio
of its boundingBox diagonal."
self subclassResponsibility!
translateBy: aPoint
self subclassResponsibility! !
!SDGraphicObject methodsFor: 'private'!
computeBoundingBox
self subclassResponsibility!
setBoundingBox: aRectangle
"Set the receiver's boundingBox and update its handles."
boundingBox := aRectangle.
self setHandles!
setHandles
"For consistency, handles should be set in a clockwise direction."
handles := Array
with: self boundingBox origin
with: self boundingBox topRight
with: self boundingBox corner
with: self boundingBox bottomLeft! !
!SDGraphicObject methodsFor: 'testing'!
animateUsingRectangle
"Answer whether a rectangle should be used in animating the receiver when scaling."
^true!
containsPoint: aPoint
^(self boundingBox containsPoint: aPoint)
ifTrue: [self isHollow
ifTrue: [self edgeContainsPoint: aPoint]
ifFalse: [(self interiorContainsPoint: aPoint)
or: [self edgeContainsPoint: aPoint]]]
ifFalse: [false]!
edgeContainsPoint: aPoint
self subclassResponsibility!
insideRectangle: aRectangle
"Answer whether the receiver is entirely inside aRectangle."
^aRectangle contains: self boundingBox!
interiorContainsPoint: aPoint
self subclassResponsibility!
isHollow
^self insideColor isNil!
isSquished
"Answer whether the receiver is either totally horizontal or totally
vertical."
^(self width isZero or: [self height isZero])!
tolerance
"Answer the minimum distance that a point can be from an edge of the
receiver to constitute a 'hit'. Actually, this number should probably be
determined by the current scale of the view because reduced views require a
larger tolerance, in which case it should be passed as an argument from the
controller."
^(self lineWidth/2) truncated + 8! !
!SDGraphicObject methodsFor: 'copying'!
deepCopy
"Answer a copy of self with copies of instance variables I define.
Subclasses that define additional instance variables may want to override me."
^self copy
insideColor: self insideColor copy;
borderColor: self borderColor copy;
lineWidth: self lineWidth copy;
handles: (self handles collect: [ :h | h copy ]);
boundingBox: self boundingBox copy;
yourself.! !
SDGraphicObject subclass: #SDLineSegment
instanceVariableNames: 'start end '
classVariableNames: ''
poolDictionaries: ''
category: 'SmallDraw'
"
The following instance variables are inherited by this class:
SDGraphicObject -- insideColor, borderColor, lineWidth, handles, boundingBox
Object --
"!
!SDLineSegment methodsFor: 'accessing'!
displayHandles
^Array with: self start with: self end!
end
^end!
start
^start!
start: thisPoint end: thatPoint
thisPoint x < thatPoint x
ifTrue: [start := thisPoint. end := thatPoint]
ifFalse: [start := thatPoint. end := thisPoint].
start := start rounded. end := end rounded.
self computeBoundingBox! !
!SDLineSegment methodsFor: 'converting'!
outline
^self displayHandles! !
!SDLineSegment methodsFor: 'displaying'!
displayOn: aGC scale: aScalePoint
self borderColor isNil
ifFalse: [aGC
paint: self borderColor;
lineWidth: self lineWidth;
displayLineFrom: (self start * aScalePoint)
to: (self end * aScalePoint)].! !
!SDLineSegment methodsFor: 'manipulation'!
scaleAbsoluteBy: aPercentagePoint aboutHandleAt: anIndex
(self handles at: anIndex) = self start
ifTrue: [self start: self start
end: self start + (aPercentagePoint * (self start dist: self end))]
ifFalse: [self start: self end + (aPercentagePoint * (self start dist: self end))
end: self end]!
scaleRelativeBy: aPercentagePoint aboutHandleAt: anIndex
| anchor |
anchor := self handles at: anIndex.
self start: ((self start - anchor) * aPercentagePoint) + anchor
end: ((self end - anchor) * aPercentagePoint) + anchor!
translateBy: aPoint
self start: self start + aPoint end: self end + aPoint! !
!SDLineSegment methodsFor: 'private'!
computeBoundingBox
self setBoundingBox: (Rectangle vertex: self start vertex: self end)! !
!SDLineSegment methodsFor: 'testing'!
animateUsingRectangle
^false!
containsPoint: aPoint
^self edgeContainsPoint: aPoint!
edgeContainsPoint: aPoint
"Answer whether any one of the receiver's edges contains aPoint. This
is true if aPoint is within a certain distance from an edge - see message:
tolerance."
^((aPoint nearestIntegerPointOnLineFrom: self start to: self end)
dist: aPoint) <= self tolerance! !
!SDLineSegment methodsFor: 'copying'!
deepCopy
"Answer a copy of self with copies of instance variables I define.
Subclasses that define additional instance variables may want to override me."
^self copy
start: self start copy
end: self end copy! !
"-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- "!
SDLineSegment class
instanceVariableNames: ''
"
The following instance variables are inherited by this class:
SDGraphicObject class -
Object class -
Class -- name, classPool, sharedPools
ClassDescription -- instanceVariables, organization
Behavior -- superclass, methodDict, format, subclasses
Object --
"!
!SDLineSegment class methodsFor: 'instance creation'!
from: thisPoint to: thatPoint
^self start: thisPoint end: thatPoint!
start: thisPoint end: thatPoint
^self new start: thisPoint end: thatPoint! !
SDGraphicObject subclass: #SDPolyline
instanceVariableNames: 'vertices '
classVariableNames: ''
poolDictionaries: ''
category: 'SmallDraw'
"
The following instance variables are inherited by this class:
SDGraphicObject -- insideColor, borderColor, lineWidth, handles, boundingBox
Object --
"!
!SDPolyline methodsFor: 'accessing'!
vertices
^vertices!
vertices: aCollectionOfPoints
vertices := aCollectionOfPoints asArray.
self computeBoundingBox! !
!SDPolyline methodsFor: 'converting'!
outline
^self vertices! !
!SDPolyline methodsFor: 'displaying'!
displayOn: aGC scale: aScalePoint
| displayPoints |
aGC joinStyle: self joinStyle.
displayPoints := self vertices collect: [:pt | pt * aScalePoint].
self insideColor isNil
ifFalse: [aGC
paint: self insideColor;
displayPolygon: displayPoints].
self borderColor isNil
ifFalse: [aGC
paint: self borderColor;
lineWidth: self lineWidth;
displayPolyline: displayPoints].! !
!SDPolyline methodsFor: 'manipulation'!
scaleAbsoluteBy: aPercentagePoint aboutHandleAt: anIndex
| anchor |
anchor := self handles at: anIndex.
self
vertices: (Array with: anchor
with: anchor + (aPercentagePoint *
(anchor dist: (self handleOpposite: anchor))))!
scaleRelativeBy: aPercentagePoint aboutHandleAt: anIndex
| anchor |
anchor := self handles at: anIndex.
self vertices: (self vertices collect: [:pt |
(((pt - anchor) * aPercentagePoint) + anchor) rounded])!
translateBy: aPoint
self vertices: (self vertices collect: [:pt | pt + aPoint])! !
!SDPolyline methodsFor: 'private'!
computeBoundingBox
| minPoint maxPoint |
minPoint := maxPoint := self vertices first.
self vertices do: [:each |
minPoint := minPoint min: each.
maxPoint := maxPoint max: each].
self setBoundingBox: (Rectangle origin: minPoint corner: maxPoint)! !
!SDPolyline methodsFor: 'testing'!
edgeContainsPoint: aPoint
"Answer whether any one of the receiver's edges contains aPoint. This
is true if aPoint is within a certain distance from an edge - see message:
tolerance."
| delta from |
delta := self tolerance.
from := self outline first.
self outline do: [:pt |
((aPoint nearestIntegerPointOnLineFrom: from to: pt ) dist: aPoint) <= delta
ifTrue: [^true].
from := pt].
^false!
interiorContainsPoint: aPoint
"Answer whether the receiver contains aPoint on its boundary or in its
interior. Uses the winding technique. See the method
Point|quadrantContaining:. "
| wind lastPoint oldQuad newQuad |
wind := 0.
lastPoint := self vertices last.
oldQuad := lastPoint quadrantContaining: aPoint.
self vertices do: [:each |
aPoint = each ifTrue: [^true].
newQuad := each quadrantContaining: aPoint.
oldQuad = newQuad
ifFalse: [oldQuad+1\\4 = newQuad
ifTrue: [wind := wind + 1]
ifFalse: [newQuad+1\\4 = oldQuad
ifTrue: [wind := wind - 1]
ifFalse: [| a b |
a := lastPoint y - each y.
a := a * (aPoint x - lastPoint x).
b := lastPoint x - each x.
a := a + (b * lastPoint y).
b := b * aPoint y.
a > b
ifTrue: [wind := wind - 2]
ifFalse: [a = b ifTrue:[^true] ifFalse: [wind := wind + 2]]]]].
oldQuad := newQuad.
lastPoint := each].
^wind isZero not! !
!SDPolyline methodsFor: 'copying'!
deepCopy
"Answer a copy of self with copies of instance variables I define.
Subclasses that define additional instance variables may want to override me."
^super copy
vertices: (self vertices collect: [ :v | v copy ]);
yourself.! !
"-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- "!
SDPolyline class
instanceVariableNames: ''
"
The following instance variables are inherited by this class:
SDGraphicObject class -
Object class -
Class -- name, classPool, sharedPools
ClassDescription -- instanceVariables, organization
Behavior -- superclass, methodDict, format, subclasses
Object --
"!
!SDPolyline class methodsFor: 'instance creation'!
vertices: aCollectionOfPoints
^self new vertices: aCollectionOfPoints! !
SDPolyline subclass: #SDPolygon
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'SmallDraw'
"
The following instance variables are inherited by this class:
SDPolyline -- vertices
SDGraphicObject -- insideColor, borderColor, lineWidth, handles, boundingBox
Object --
"!
!SDPolygon methodsFor: 'private'!
vertices: aCollectionOfPoints
"This just makes sure that the receiver's vertices are closed."
super vertices: (aCollectionOfPoints first = aCollectionOfPoints last
ifTrue: [aCollectionOfPoints]
ifFalse: [aCollectionOfPoints
asOrderedCollection add: aCollectionOfPoints first; yourself]) asArray! !
SDPolygon subclass: #SDQuadrangle
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'SmallDraw'
"
The following instance variables are inherited by this class:
SDPolygon -
SDPolyline -- vertices
SDGraphicObject -- insideColor, borderColor, lineWidth, handles, boundingBox
Object --
"!
!SDQuadrangle methodsFor: 'accessing'!
origin: aPoint corner: anotherPoint
self vertex: aPoint vertex: anotherPoint!
vertex: aPoint vertex: anotherPoint
"Don't allow zero-size."
| r |
r := (Rectangle vertex: aPoint vertex: (aPoint = anotherPoint
ifTrue: [aPoint + 1]
ifFalse: [anotherPoint])) rounded.
self vertices: (Array
with: r topLeft
with: r topRight
with: r bottomRight
with: r bottomLeft)! !
!SDQuadrangle methodsFor: 'displaying'!
joinStyle
"Answer the appropriate join style for displaying the receiver."
^GraphicsContext joinMiter! !
!SDQuadrangle methodsFor: 'manipulation'!
scaleAbsoluteBy: aPercentagePoint aboutHandleAt: anIndex
| anchor |
anchor := self handles at: anIndex.
self vertex: anchor
vertex: anchor + (aPercentagePoint * (anchor dist: (self handleOpposite: anchor)))! !
!SDQuadrangle methodsFor: 'testing'!
interiorContainsPoint: aPoint
"This is valid as long as SDQuadrangles are aligned with the x-y axis."
^self boundingBox containsPoint: aPoint! !
"-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- "!
SDQuadrangle class
instanceVariableNames: ''
"
The following instance variables are inherited by this class:
SDPolygon class -
SDPolyline class -
SDGraphicObject class -
Object class -
Class -- name, classPool, sharedPools
ClassDescription -- instanceVariables, organization
Behavior -- superclass, methodDict, format, subclasses
Object --
"!
!SDQuadrangle class methodsFor: 'instance creation'!
origin: aPoint corner: anotherPoint
^self new origin: aPoint corner: anotherPoint! !
SDQuadrangle subclass: #SDEllipse
instanceVariableNames: ''
classVariableNames: 'UnitCircle '
poolDictionaries: ''
category: 'SmallDraw'
"
The following instance variables are inherited by this class:
SDQuadrangle -
SDPolygon -
SDPolyline -- vertices
SDGraphicObject -- insideColor, borderColor, lineWidth, handles, boundingBox
Object --
"!
!SDEllipse methodsFor: 'accessing'!
xRadius
^self width / 2!
yRadius
^self height / 2! !
!SDEllipse methodsFor: 'converting'!
outline
"Answer an array of points that represent the receiver as an outline."
^UnitCircle collect: [:pt | self center + (pt * (self xRadius@self yRadius))]! !
!SDEllipse methodsFor: 'displaying'!
displayOn: aGC scale: aScalePoint
| bb |
bb := self boundingBox scaledBy: aScalePoint.
self insideColor isNil
ifFalse: [aGC
paint: self insideColor;
displayWedgeBoundedBy: bb startAngle: 0
sweepAngle: 360 at: 0@0].
self borderColor isNil
ifFalse: [aGC
lineWidth: self lineWidth;
paint: self borderColor.
self isSquished
ifTrue: [aGC displayLineFrom: bb origin to: bb corner]
ifFalse: [aGC
displayArcBoundedBy: bb
startAngle: 0
sweepAngle: 360
at:0@0]]! !
!SDEllipse methodsFor: 'testing'!
edgeContainsPoint: aPoint
^self isCircle
ifTrue: [(self center dist: aPoint) <= (self xRadius + self tolerance)]
ifFalse: [| offset constantLength |
"Determine focus points on major axis."
self width >= self height
ifTrue: [offset := (self xRadius squared - self yRadius squared) sqrt@0.
constantLength := self width]
ifFalse: [offset := 0@(self yRadius squared - self xRadius squared) sqrt.
constantLength := self height].
"Now, answer whether the sum of the distances between aPoint and the two focus
points is close enough to the constantLength."
((((self center - offset) dist: aPoint) +
((self center + offset) dist: aPoint)) -
constantLength)
abs < self tolerance]!
interiorContainsPoint: aPoint
^self isCircle
ifTrue: [(self center dist: aPoint) <= self xRadius]
ifFalse: [| offset constantLength |
"Determine focus points on major axis."
self width >= self height
ifTrue: [offset := (self xRadius squared - self yRadius squared) sqrt@0.
constantLength := self width]
ifFalse: [offset := 0@(self yRadius squared - self xRadius squared) sqrt.
constantLength := self height].
"Now, answer whether the sum of the distances between aPoint and the two focus
points is less than the constantLength."
(((self center - offset) dist: aPoint) +
((self center + offset) dist: aPoint)) <= constantLength]!
isCircle
^self width = self height! !
"-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- "!
SDEllipse class
instanceVariableNames: ''
"
The following instance variables are inherited by this class:
SDQuadrangle class -
SDPolygon class -
SDPolyline class -
SDGraphicObject class -
Object class -
Class -- name, classPool, sharedPools
ClassDescription -- instanceVariables, organization
Behavior -- superclass, methodDict, format, subclasses
Object --
"!
!SDEllipse class methodsFor: 'initialize'!
initialize
"Construct an array of points that describe a unit circle."
UnitCircle := OrderedCollection new: 37.
0 to: 2*Float pi by: Float pi / 18 do: [:a | UnitCircle add: (a cos @ a sin)].
UnitCircle addLast: UnitCircle first.
UnitCircle := UnitCircle asArray! !
SDGraphicObject subclass: #SDGraphicGroup
instanceVariableNames: 'elements '
classVariableNames: ''
poolDictionaries: ''
category: 'SmallDraw'
"
The following instance variables are inherited by this class:
SDGraphicObject -- insideColor, borderColor, lineWidth, handles, boundingBox
Object --
"!
!SDGraphicGroup methodsFor: 'accessing'!
borderColor: aColorValue
self elements do: [:o | o borderColor: aColorValue]!
elements
^elements!
elements: aCollectionOfGraphicObjects
elements := aCollectionOfGraphicObjects.
self computeBoundingBox!
insideColor: aColorValue
self elements do: [:o | o insideColor: aColorValue]!
lineWidth: anInteger
self elements do: [:o | o lineWidth: anInteger]! !
!SDGraphicGroup methodsFor: 'displaying'!
displayOn: aGC scale: aScalePoint
self elements do: [:o | o displayOn: aGC scale: aScalePoint]! !
!SDGraphicGroup methodsFor: 'manipulation'!
scaleAbsoluteBy: aPercentagePoint aboutHandleAt: anIndex
"aPercentagePoint = (unitVector) * (newDiagonalDistance / oldDiagonalDistance)"
| anchor corner difference anchorHandle oldHandle newPoint unitVector
scale uv length newLength len perc delta |
anchor := self handles at: anIndex.
corner := anchor + (aPercentagePoint * (anchor dist: (self handleOpposite: anchor))).
difference := corner - (self handleOpposite: anchor).
uv := (anchor - (self handleOpposite: anchor)) unitVector.
length := anchor dist: (self handleOpposite: anchor).
newLength := length + (difference * uv).
self elements do: [:eachObject |
anchorHandle := eachObject handles at: anIndex.
oldHandle := eachObject handleOpposite: anchorHandle.
newPoint := oldHandle + difference.
unitVector := (newPoint - anchorHandle) unitVector.
scale := (newPoint dist: anchorHandle) / (oldHandle dist: anchorHandle).
len := anchorHandle dist: anchor.
perc := len / length.
delta := (perc * newLength) + anchorHandle.
eachObject
translateBy: delta;
scaleAbsoluteBy: (unitVector * scale) aboutHandleAt: anIndex].
self computeBoundingBox!
scaleRelativeBy: aPercentagePoint aboutHandleAt: anIndex
"When a group is stretched, each member of the group must maintain its
relative position within the group. This is different than stretching
individual objects whose handle at anIndex remains stationary."
| origin corner groupDiagonal newDiagonal objectDiagonal |
origin := self handles at: anIndex.
corner := self handleOpposite: origin.
groupDiagonal := corner - origin.
newDiagonal := groupDiagonal * aPercentagePoint.
self elements do: [:eachObject |
objectDiagonal := (eachObject handles at: anIndex) - origin.
eachObject
translateBy: (newDiagonal * objectDiagonal / groupDiagonal) - objectDiagonal;
scaleRelativeBy: aPercentagePoint aboutHandleAt: anIndex].
self computeBoundingBox!
translateBy: aPoint
self elements do: [:o | o translateBy: aPoint].
self computeBoundingBox! !
!SDGraphicGroup methodsFor: 'private'!
computeBoundingBox
self setBoundingBox:
(self elements inject: self elements first boundingBox into:
[:bb :o | bb merge: o boundingBox])! !
!SDGraphicGroup methodsFor: 'testing'!
containsPoint: aPoint
^(self elements detect: [:eachObject | eachObject containsPoint: aPoint]
ifNone: [nil]) notNil! !
!SDGraphicGroup methodsFor: 'copying'!
deepCopy
"Answer a copy of self with copies of instance variables I define.
Subclasses that define additional instance variables may want to override me."
^super copy
elements: (self elements collect: [ :e | e copy ]);
yourself.! !
"-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- "!
SDGraphicGroup class
instanceVariableNames: ''
"
The following instance variables are inherited by this class:
SDGraphicObject class -
Object class -
Class -- name, classPool, sharedPools
ClassDescription -- instanceVariables, organization
Behavior -- superclass, methodDict, format, subclasses
Object --
"!
!SDGraphicGroup class methodsFor: 'instance creation'!
with: aCollectionOfGraphicObjects
^self new elements: aCollectionOfGraphicObjects! !
!Point methodsFor: 'SmallDraw additions'!
quadrantContaining: aPoint
"Answer the number of the quadrant containing aPoint placing the
receiver at the origin, where the quadrants are numbered as follows:
1 | 0
------
2 | 3
This convention is used for determining whether a point is in a polygon."
^aPoint x > x
ifTrue: [aPoint y >= y
ifTrue: [3]
ifFalse: [0]]
ifFalse: [aPoint y >= y
ifTrue: [2]
ifFalse: [1]]! !
SmallDrawView initialize!
SDEllipse initialize!